Skip to content

Use Yutori native tool sets - #15

Merged
rgarcia merged 3 commits into
mainfrom
hypeship/yutori-native-tools
May 20, 2026
Merged

Use Yutori native tool sets#15
rgarcia merged 3 commits into
mainfrom
hypeship/yutori-native-tools

Conversation

@rgarcia

@rgarcia rgarcia commented May 14, 2026

Copy link
Copy Markdown
Contributor

Summary

  • model Yutori Navigator n1/n1.5 native action sets and expose native-to-canonical action normalization helpers
  • switch the Yutori provider to native tool_set mode instead of sending default CUA batch/navigation browser tools
  • normalize Yutori native tool calls to canonical individual CUA tool calls and update docs/tests

Tests

  • npm run build --workspace @onkernel/cua-ai
  • npm test --workspace @onkernel/cua-ai
  • npx tsc -b
  • npm test --workspace @onkernel/cua-agent
  • YUTORI_API_KEY=<set> npx vitest --run test/batch-tool.integration.test.ts -t yutori from packages/ai
  • npm run build was not completed because the optional @onkernel/ptywright native build requires zig; TypeScript compilation passed via npx tsc -b

Note

Medium Risk
Moderate risk because it changes tool schemas/names and provider payload middleware (Yutori/Tzafon) plus adds screenshot transformation/coordinate normalization in the agent executor, which can break existing integrations and runtime behavior across providers.

Overview
Switches CUA from a batch/navigation-centric tool surface to canonical per-action tools (e.g. click, screenshot) via the new computerTools() APIs and updated docs/examples/tests.

Updates provider runtime specs and payload adapters: resolveCuaRuntimeSpec() now returns per-provider coordinateSystem, optional screenshot policy (Yutori appends a transformed WebP screenshot to the latest message), and a new payload hook context (keepToolNames) so providers can strip local executor tools while preserving caller tools. Yutori now uses native tool_set mode with native→canonical action normalization; Tzafon now injects its native computer_use tool and maps computer_call outputs into canonical tool calls.

Enhances @onkernel/cua-agent execution: adds extraTools, optional synthesized batch_computer_actions/computer_use_extra, executes individual action tools locally, normalizes key combos, supports provider coordinate denormalization, normalizes bare goto URLs, and adds screenshot transforms (via new sharp dependency). CI integration coverage is updated to run the new computer-tool.integration.test.ts.

Reviewed by Cursor Bugbot for commit 53f7e28. Bugbot is set up for automated code reviews on this repo. Configure here.

@rgarcia
rgarcia marked this pull request as ready for review May 14, 2026 15:20
@firetiger-agent

Copy link
Copy Markdown

Firetiger deploy monitoring skipped

This PR didn't match the auto-monitor filter configured on your GitHub connection:

Any PR that changes the kernel API. Monitor changes to API endpoints (packages/api/cmd/api/) and Temporal workflows (packages/api/lib/temporal) in the kernel repo

Reason: PR modifies AI/CUA tool sets and Yutori provider integration, not kernel API endpoints or Temporal workflows as specified in the filter.

To monitor this PR anyway, reply with @firetiger monitor this.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Inconsistent modifier key naming within same file
    • Updated clear_before_typing keypress generation to use the same normalized ctrl modifier mapping used by hold_keys paths and adjusted the unit test expectation accordingly.

Create PR

Or push these changes by commenting:

@cursor push 3049347a8e
Preview (3049347a8e)
diff --git a/packages/ai/src/providers/yutori/actions.ts b/packages/ai/src/providers/yutori/actions.ts
--- a/packages/ai/src/providers/yutori/actions.ts
+++ b/packages/ai/src/providers/yutori/actions.ts
@@ -195,7 +195,7 @@
 	if (text === undefined) return undefined;
 	const actions: CuaAction[] = [];
 	if (args.clear_before_typing === true) {
-		actions.push({ type: "keypress", keys: ["Control", "a"] }, { type: "keypress", keys: ["Backspace"] });
+		actions.push({ type: "keypress", keys: [MODIFIER_MAP.control, "a"] }, { type: "keypress", keys: ["Backspace"] });
 	}
 	actions.push({ type: "type", text });
 	if (args.press_enter_after === true) actions.push({ type: "keypress", keys: ["Enter"] });

diff --git a/packages/ai/test/yutori-actions.test.ts b/packages/ai/test/yutori-actions.test.ts
--- a/packages/ai/test/yutori-actions.test.ts
+++ b/packages/ai/test/yutori-actions.test.ts
@@ -26,7 +26,7 @@
 			{ type: "drag", path: [{ x: 100, y: 200 }, { x: 300, y: 400 }], button: "left" },
 		]);
 		expect(yutori.toCanonicalActions("type", { text: "hello", clear_before_typing: true, press_enter_after: true })).toEqual([
-			{ type: "keypress", keys: ["Control", "a"] },
+			{ type: "keypress", keys: ["ctrl", "a"] },
 			{ type: "keypress", keys: ["Backspace"] },
 			{ type: "type", text: "hello" },
 			{ type: "keypress", keys: ["Enter"] },

You can send follow-ups to the cloud agent here.

Comment thread packages/ai/src/providers/yutori/actions.ts Outdated
Comment thread packages/ai/src/providers/yutori/actions.ts
const SCROLL_AMOUNT_PER_NOTCH = 120;
const DEFAULT_WAIT_MS = 2000;
const NAVIGATION_WAIT_MS = 1500;
const GOTO_WAIT_MS = 2000;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment on what these consts control and rhyme/reason

const NAVIGATION_WAIT_MS = 1500;
const GOTO_WAIT_MS = 2000;

const MODIFIER_MAP: Record<string, string> = {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems like somethign we should pull out to a common layer--presumably other providers will spit out modifier keys of all kinds and we want to normalize to a canonical set, similar to what we do for actions

different from actions though i'd be fine doing this normalization in some logic that is basically a catchall / union of everything we may come across across all providers

remember that the canonical modifier set should be x11 keysyms since that's what kernel uses under the hood

super: "super",
};

export function createComputerToolDefinitions(_options?: unknown): [] {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment explaining why this is empty

);
}

export function toCanonicalActions(name: string, args: Record<string, unknown>): CuaAction[] | undefined {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should have a test that this is exhaustive, at least for YUTORI_N15_CORE_ACTION_TYPES

and we should be flexible to expand the CuaAction common types for things not modeled

export const COMPUTER_TOOL_COORDINATES = { type: "normalized", range: [0, 1000] } as const satisfies ComputerToolCoordinateSystem;

export const YUTORI_INSTRUCTIONS_RAW = `You control a Kernel cloud browser. Prefer batched computer actions for browser interaction and include screenshot or URL reads when you need updated state.`;
export const YUTORI_INSTRUCTIONS_RAW = "";

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment on why (yutori recommends this, link to docs)

@rgarcia

rgarcia commented May 14, 2026

Copy link
Copy Markdown
Contributor Author

addressed the review comments in 807c63c:

  • added doc-backed tsdoc/comments for the Yutori n1/n1.5 action sets, tool_set ids, disabled expanded/ref tools, empty createComputerToolDefinitions, and empty default system prompt
  • moved key/modifier normalization into providers/common.ts, with canonical modifier output as X11 keysyms
  • updated Yutori native action normalization to use common key normalization, including sequential key expressions like down down enter
  • added an exhaustive n1.5 core-action normalization test table
  • wired canonical hold_keys and keypress.duration through the agent translator so the normalized action fields reach Kernel batch actions

verified:

  • npm run build --workspace @onkernel/cua-ai
  • npm run build --workspace @onkernel/cua-agent
  • npm test --workspace @onkernel/cua-ai
  • npm test --workspace @onkernel/cua-agent
  • YUTORI_API_KEY=<set> npx vitest --run test/batch-tool.integration.test.ts -t yutori from packages/ai

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Duplicate key alias maps already diverge on entries
    • The translator now reuses normalizeCuaKey from @onkernel/cua-ai and super_l was added to CUA_KEY_ALIASES, eliminating the divergent alias mapping path.
  • ✅ Fixed: Exported functions and types lack TSDoc comments
    • Added TSDoc comments for the flagged exported types and normalization/mapping helpers in common.ts and yutori/actions.ts.

Create PR

Or push these changes by commenting:

@cursor push 02e28e8ce1
Preview (02e28e8ce1)
diff --git a/packages/agent/src/translator/translator.ts b/packages/agent/src/translator/translator.ts
--- a/packages/agent/src/translator/translator.ts
+++ b/packages/agent/src/translator/translator.ts
@@ -1,5 +1,6 @@
 import type Kernel from "@onkernel/sdk";
 import type { BrowserCreateResponse, BrowserRetrieveResponse } from "@onkernel/sdk/resources/browsers";
+import { normalizeCuaKey } from "@onkernel/cua-ai";
 import type { BatchExecutionResult, ModelAction } from "./types";
 
 export type KernelBrowser = BrowserCreateResponse | BrowserRetrieveResponse;
@@ -230,48 +231,13 @@
 	};
 }
 
-const KEY_ALIASES: Record<string, string> = {
-	ctrl: "Control_L",
-	control: "Control_L",
-	control_l: "Control_L",
-	controlleft: "Control_L",
-	alt: "Alt_L",
-	alt_l: "Alt_L",
-	altleft: "Alt_L",
-	shift: "Shift_L",
-	shift_l: "Shift_L",
-	shiftleft: "Shift_L",
-	meta: "Super_L",
-	super: "Super_L",
-	super_l: "Super_L",
-	cmd: "Super_L",
-	command: "Super_L",
-	enter: "Return",
-	return: "Return",
-	escape: "Escape",
-	esc: "Escape",
-	backspace: "BackSpace",
-	delete: "Delete",
-	tab: "Tab",
-	space: "space",
-	left: "Left",
-	right: "Right",
-	up: "Up",
-	down: "Down",
-};
-
 function translateKeys(keys: string[]): string[] {
 	return keys.flatMap((key) =>
 		key
 			.split("+")
 			.map((part) => part.trim())
 			.filter(Boolean)
-			.map((part) => {
-				const alias = KEY_ALIASES[part.replace(/[-\s]/g, "_").toLowerCase()];
-				if (alias) return alias;
-				if (part.length === 1 && part >= "A" && part <= "Z") return part.toLowerCase();
-				return part;
-			}),
+			.map((part) => normalizeCuaKey(part)),
 	);
 }
 

diff --git a/packages/ai/src/providers/common.ts b/packages/ai/src/providers/common.ts
--- a/packages/ai/src/providers/common.ts
+++ b/packages/ai/src/providers/common.ts
@@ -135,6 +135,7 @@
 
 export const CUA_MODIFIER_KEYSYMS = ["Control_L", "Alt_L", "Shift_L", "Super_L"] as const;
 
+/** Canonical X11 keysyms accepted by `hold_keys` and modifier-only keypresses. */
 export type CuaModifierKeysym = (typeof CUA_MODIFIER_KEYSYMS)[number];
 
 const CUA_KEY_ALIASES: Record<string, string> = {
@@ -169,6 +170,7 @@
 	shiftleft: "Shift_L",
 	space: "space",
 	super: "Super_L",
+	super_l: "Super_L",
 	tab: "Tab",
 	up: "Up",
 };
@@ -191,11 +193,13 @@
 	return trimmed;
 }
 
+/** Resolve a raw key string to a canonical modifier keysym when possible. */
 export function normalizeCuaModifierKey(value: string): CuaModifierKeysym | undefined {
 	const key = normalizeCuaKey(value);
 	return CUA_MODIFIER_KEYSYM_SET.has(key) ? (key as CuaModifierKeysym) : undefined;
 }
 
+/** Normalize a `+`-delimited key combination (for example `ctrl+shift+tab`). */
 export function normalizeCuaKeyCombo(value: string): string[] {
 	return value
 		.split("+")
@@ -203,6 +207,7 @@
 		.filter(Boolean);
 }
 
+/** Normalize whitespace-delimited key combinations into sequential chord presses. */
 export function normalizeCuaKeySequence(value: string): string[][] {
 	return value
 		.split(/\s+/)

diff --git a/packages/ai/src/providers/yutori/actions.ts b/packages/ai/src/providers/yutori/actions.ts
--- a/packages/ai/src/providers/yutori/actions.ts
+++ b/packages/ai/src/providers/yutori/actions.ts
@@ -85,9 +85,13 @@
 	...YUTORI_N15_EXPANDED_ACTION_TYPES,
 ] as const;
 
+/** Native action names emitted by Yutori Navigator n1. */
 export type YutoriN1ActionType = (typeof YUTORI_N1_ACTION_TYPES)[number];
+/** Native action names emitted by Yutori Navigator n1.5 core tool set. */
 export type YutoriN15CoreActionType = (typeof YUTORI_N15_CORE_ACTION_TYPES)[number];
+/** Native action names emitted by Yutori Navigator n1.5 expanded tool set. */
 export type YutoriN15ExpandedActionType = (typeof YUTORI_N15_EXPANDED_ACTION_TYPES)[number];
+/** Union of all provider-native action names this adapter can normalize. */
 export type YutoriNativeActionType = YutoriN1ActionType | YutoriN15CoreActionType | YutoriN15ExpandedActionType;
 
 const YUTORI_NATIVE_ACTION_NAMES = new Set<string>([
@@ -110,18 +114,22 @@
 	return [];
 }
 
+/** Return the Yutori `tool_set` id required by a model, if any. */
 export function yutoriToolSetForModel(modelId: string): typeof YUTORI_N15_CORE_TOOL_SET | undefined {
 	return modelId.startsWith("n1.5") ? YUTORI_N15_CORE_TOOL_SET : undefined;
 }
 
+/** Return the provider-native action names available for the selected model. */
 export function yutoriNativeActionsForModel(modelId: string): readonly YutoriNativeActionType[] {
 	return modelId.startsWith("n1.5") ? YUTORI_N15_CORE_ACTION_TYPES : YUTORI_N1_ACTION_TYPES;
 }
 
+/** Check whether a tool call name belongs to Yutori's native action vocabulary. */
 export function isYutoriNativeActionName(name: string): boolean {
 	return YUTORI_NATIVE_ACTION_NAMES.has(name);
 }
 
+/** Check whether a tool name is one of CUA's default computer-use tools. */
 export function isCuaDefaultToolName(name: string): boolean {
 	return (
 		name === CUA_BATCH_TOOL_NAME ||
@@ -130,6 +138,7 @@
 	);
 }
 
+/** Convert a Yutori native action payload into canonical CUA actions. */
 export function toCanonicalActions(name: string, args: Record<string, unknown>): CuaAction[] | undefined {
 	const coords = readPoint(args.coordinates);
 	switch (name) {
@@ -184,10 +193,12 @@
 	}
 }
 
+/** Resolve canonical tool call name from a canonical CUA action. */
 export function canonicalToolCallName(action: CuaAction): CuaActionType {
 	return action.type;
 }
 
+/** Strip the action discriminant and return canonical tool arguments. */
 export function canonicalToolCallArguments(action: CuaAction): Record<string, unknown> {
 	const { type: _type, ...args } = action as CuaAction & Record<string, unknown>;
 	return args;

diff --git a/packages/ai/test/common-keys.test.ts b/packages/ai/test/common-keys.test.ts
--- a/packages/ai/test/common-keys.test.ts
+++ b/packages/ai/test/common-keys.test.ts
@@ -10,6 +10,7 @@
 	it("normalizes common provider key names to X11 keysyms", () => {
 		expect(normalizeCuaModifierKey("ctrl")).toBe("Control_L");
 		expect(normalizeCuaModifierKey("command")).toBe("Super_L");
+		expect(normalizeCuaModifierKey("super_l")).toBe("Super_L");
 		expect(normalizeCuaKey("Backspace")).toBe("BackSpace");
 		expect(normalizeCuaKey("ArrowLeft")).toBe("Left");
 		expect(normalizeCuaKey("enter")).toBe("Return");

You can send follow-ups to the cloud agent here.

Comment thread packages/ai/src/providers/common.ts Outdated
export function canonicalToolCallArguments(action: CuaAction): Record<string, unknown> {
const { type: _type, ...args } = action as CuaAction & Record<string, unknown>;
return args;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exported functions and types lack TSDoc comments

Low Severity

Multiple new exported functions (yutoriToolSetForModel, yutoriNativeActionsForModel, isYutoriNativeActionName, isCuaDefaultToolName, toCanonicalActions, canonicalToolCallName, canonicalToolCallArguments) and exported types (YutoriN1ActionType, YutoriN15CoreActionType, YutoriNativeActionType, CuaModifierKeysym) lack TSDoc comments. Exported functions like normalizeCuaModifierKey, normalizeCuaKeyCombo, and normalizeCuaKeySequence in common.ts are also missing TSDoc. This violates the rule requiring TSDoc on all exported types, interfaces, and classes.

Additional Locations (1)
Fix in Cursor Fix in Web

Triggered by learned rule: Exported types, interfaces, and classes require TSDoc

Reviewed by Cursor Bugbot for commit 807c63c. Configure here.

@rgarcia

rgarcia commented May 14, 2026

Copy link
Copy Markdown
Contributor Author

Updated this PR with the follow-up Yutori runtime work:

  • exposes provider runtime coordinate/screenshot policy through @onkernel/cua-ai and consumes it generically in @onkernel/cua-agent
  • installs local canonical Yutori action executors while filtering those definitions from Yutori API payloads in favor of tool_set
  • moves key normalization to the agent execution layer and expands it with Yutori SDK-style key aliases
  • normalizes bare goto URLs before execution
  • adds unit coverage for Yutori payload filtering, screenshot preparation, coordinate denormalization, and direct local canonical action execution

Validation:

  • npm run build --workspace @onkernel/cua-ai
  • npm test --workspace @onkernel/cua-ai
  • npm run build --workspace @onkernel/cua-agent
  • npm test --workspace @onkernel/cua-agent
  • yutori live e2e slice for CuaAgent and CuaAgentHarness

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 3 total unresolved issues (including 1 from previous review).

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Exported function isYutoriNativeActionName is never used
    • Removed the unused isYutoriNativeActionName export and its backing set from Yutori actions to eliminate dead code.
  • ✅ Fixed: Duplicate normalizeGotoUrl across two packages
    • Extracted a single shared normalizeGotoUrl helper in providers/common and updated both Yutori actions and agent translator to use it.

Create PR

Or push these changes by commenting:

@cursor push 09af3d5ea8
Preview (09af3d5ea8)
diff --git a/packages/agent/src/translator/translator.ts b/packages/agent/src/translator/translator.ts
--- a/packages/agent/src/translator/translator.ts
+++ b/packages/agent/src/translator/translator.ts
@@ -1,6 +1,6 @@
 import type Kernel from "@onkernel/sdk";
 import type { BrowserCreateResponse, BrowserRetrieveResponse } from "@onkernel/sdk/resources/browsers";
-import type { ComputerToolCoordinateSystem, CuaScreenshotSpec } from "@onkernel/cua-ai";
+import { normalizeGotoUrl, type ComputerToolCoordinateSystem, type CuaScreenshotSpec } from "@onkernel/cua-ai";
 import sharp from "sharp";
 import { isKernelModifierKey, normalizeKernelKey, normalizeKernelKeyCombo } from "./keys";
 import type { BatchExecutionResult, ModelAction } from "./types";
@@ -98,7 +98,7 @@
 				continue;
 			}
 			if (type === "goto") {
-				const url = normalizeGotoUrl(stringOr(action.url, ""));
+				const url = normalizeGotoUrl(stringOr(action.url, "")) ?? "";
 				pending.push(
 					keypress(["Control", "l"]),
 					{ type: "type_text", type_text: { text: url } },
@@ -241,12 +241,6 @@
 	return typeof value === "string" && value.length > 0 ? value : fallback;
 }
 
-function normalizeGotoUrl(value: string): string {
-	const url = value.trim();
-	if (!url || /^[a-z][a-z0-9+.-]*:\/\//i.test(url)) return url;
-	return `https://${url}`;
-}
-
 function clickMouseButtonOr(value: unknown, fallback: ClickMouseButton): ClickMouseButton {
 	const candidate = stringOr(value, fallback);
 	if (candidate === "left" || candidate === "right" || candidate === "middle" || candidate === "back" || candidate === "forward") {

diff --git a/packages/ai/src/providers/common.ts b/packages/ai/src/providers/common.ts
--- a/packages/ai/src/providers/common.ts
+++ b/packages/ai/src/providers/common.ts
@@ -293,6 +293,13 @@
 
 export const CUA_NAVIGATION_TOOL_DESCRIPTION = "High-level browser navigation helpers for goto, back, forward, and url.";
 
+export function normalizeGotoUrl(value: unknown): string | undefined {
+	if (typeof value !== "string") return undefined;
+	const url = value.trim();
+	if (!url) return undefined;
+	return /^[a-z][a-z0-9+.-]*:\/\//i.test(url) ? url : `https://${url}`;
+}
+
 export interface CreateComputerToolDefinitionsOptions {
 	actions?: readonly CuaActionType[];
 }

diff --git a/packages/ai/src/providers/yutori/actions.ts b/packages/ai/src/providers/yutori/actions.ts
--- a/packages/ai/src/providers/yutori/actions.ts
+++ b/packages/ai/src/providers/yutori/actions.ts
@@ -3,6 +3,7 @@
 	CUA_BATCH_TOOL_NAME,
 	CUA_NAVIGATION_TOOL_NAME,
 	createCuaActionToolDefinitions,
+	normalizeGotoUrl,
 	type CuaAction,
 	type CuaActionType,
 } from "../common";
@@ -104,11 +105,6 @@
 export type YutoriN15ExpandedActionType = (typeof YUTORI_N15_EXPANDED_ACTION_TYPES)[number];
 export type YutoriNativeActionType = YutoriN1ActionType | YutoriN15CoreActionType | YutoriN15ExpandedActionType;
 
-const YUTORI_NATIVE_ACTION_NAMES = new Set<string>([
-	...YUTORI_N1_ACTION_TYPES,
-	...YUTORI_N15_ACTION_TYPES,
-]);
-
 const DEFAULT_SCROLL_AMOUNT = 3;
 const SCROLL_AMOUNT_PER_NOTCH = 120;
 const DEFAULT_WAIT_MS = 2000;
@@ -132,10 +128,6 @@
 	return modelId.startsWith("n1.5") ? YUTORI_N15_CORE_ACTION_TYPES : YUTORI_N1_ACTION_TYPES;
 }
 
-export function isYutoriNativeActionName(name: string): boolean {
-	return YUTORI_NATIVE_ACTION_NAMES.has(name);
-}
-
 export function isCuaDefaultToolName(name: string): boolean {
 	return (
 		name === CUA_BATCH_TOOL_NAME ||
@@ -270,13 +262,6 @@
 	return key ? { hold_keys: [key] } : {};
 }
 
-function normalizeGotoUrl(value: unknown): string | undefined {
-	if (typeof value !== "string") return undefined;
-	const url = value.trim();
-	if (!url) return undefined;
-	return /^[a-z][a-z0-9+.-]*:\/\//i.test(url) ? url : `https://${url}`;
-}
-
 function secondsToMs(value: unknown, fallback: number): number {
 	if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return fallback;
 	return Math.round(value * 1000);

diff --git a/packages/ai/src/providers/yutori/index.ts b/packages/ai/src/providers/yutori/index.ts
--- a/packages/ai/src/providers/yutori/index.ts
+++ b/packages/ai/src/providers/yutori/index.ts
@@ -2,7 +2,6 @@
 
 export {
 	createComputerToolDefinitions,
-	isYutoriNativeActionName,
 	toCanonicalActions,
 	yutoriNativeActionsForModel,
 	yutoriToolSetForModel,

You can send follow-ups to the cloud agent here.

Comment thread packages/ai/src/providers/yutori/actions.ts Outdated
Comment thread packages/agent/src/translator/translator.ts Outdated
@rgarcia
rgarcia force-pushed the hypeship/yutori-native-tools branch from 0565d6f to c6ed21c Compare May 15, 2026 17:04

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 3 total unresolved issues (including 1 from previous review).

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Yutori tool-set payload hook runs twice per request
    • Added a per-payload symbol marker so yutoriNativeToolSetOnPayload applies only once across internal and composed hook paths, and strip the marker before sending the request.
  • ✅ Fixed: New exported interface lacks TSDoc documentation
    • Added interface-level and field-level TSDoc comments to CuaScreenshotTransformSpec describing its screenshot transform contract.

Create PR

Or push these changes by commenting:

@cursor push c4ea5804c4
Preview (c4ea5804c4)
diff --git a/packages/ai/src/providers/yutori/provider.ts b/packages/ai/src/providers/yutori/provider.ts
--- a/packages/ai/src/providers/yutori/provider.ts
+++ b/packages/ai/src/providers/yutori/provider.ts
@@ -44,8 +44,13 @@
 	return yutoriNativeToolSetOnPayload(payload, model);
 }
 
+const YUTORI_NATIVE_TOOL_SET_APPLIED = Symbol("yutoriNativeToolSetApplied");
+
+type YutoriPayload = Record<string, unknown> & { [YUTORI_NATIVE_TOOL_SET_APPLIED]?: true };
+
 export function yutoriNativeToolSetOnPayload(payload: unknown, model?: Model<Api>): unknown | undefined {
 	if (!payload || typeof payload !== "object") return undefined;
+	if ((payload as YutoriPayload)[YUTORI_NATIVE_TOOL_SET_APPLIED]) return undefined;
 	const current = payload as { tools?: unknown };
 	const tools = Array.isArray(current.tools)
 		? current.tools.filter((tool) => {
@@ -58,7 +63,8 @@
 		...(payload as Record<string, unknown>),
 		...(toolSet ? { tool_set: toolSet, disable_tools: [...YUTORI_N15_EXPANDED_ACTION_TYPES] } : {}),
 		...(tools && tools.length > 0 ? { tools } : { tools: undefined }),
-	};
+		[YUTORI_NATIVE_TOOL_SET_APPLIED]: true,
+	} satisfies YutoriPayload;
 }
 
 async function runYutoriStream(
@@ -84,9 +90,14 @@
 		};
 		const tools = convertTools(context);
 		if (tools.length > 0) payload.tools = tools;
-		payload = yutoriNativeToolSetOnPayload(payload, model) as Record<string, unknown>;
+		const nativePayload = yutoriNativeToolSetOnPayload(payload, model);
+		if (nativePayload !== undefined) payload = nativePayload as Record<string, unknown>;
 		const nextPayload = await options?.onPayload?.(payload, model);
 		if (nextPayload !== undefined) payload = nextPayload as Record<string, unknown>;
+		const payloadWithMarker = payload as YutoriPayload;
+		if (payloadWithMarker[YUTORI_NATIVE_TOOL_SET_APPLIED]) {
+			delete payloadWithMarker[YUTORI_NATIVE_TOOL_SET_APPLIED];
+		}
 
 		const { data: response, response: rawResponse } = await client.chat.completions
 			.create(payload as unknown as Parameters<typeof client.chat.completions.create>[0], { signal: options?.signal })

diff --git a/packages/ai/src/runtime-spec.ts b/packages/ai/src/runtime-spec.ts
--- a/packages/ai/src/runtime-spec.ts
+++ b/packages/ai/src/runtime-spec.ts
@@ -7,10 +7,17 @@
 import * as yutori from "./providers/yutori/index";
 import type { ComputerToolCoordinateSystem } from "./providers/common";
 
+/**
+ * Image transform options applied to captured screenshots before provider upload.
+ */
 export interface CuaScreenshotTransformSpec {
+	/** Target image width in pixels. */
 	width: number;
+	/** Target image height in pixels. */
 	height: number;
+	/** Output encoding sent to the provider. */
 	format: "png" | "jpeg" | "webp";
+	/** Optional lossy quality setting used by `jpeg` and `webp` encoders. */
 	quality?: number;
 }
 

diff --git a/packages/ai/test/yutori-payload.test.ts b/packages/ai/test/yutori-payload.test.ts
--- a/packages/ai/test/yutori-payload.test.ts
+++ b/packages/ai/test/yutori-payload.test.ts
@@ -30,6 +30,15 @@
 		expect(next.tools).toBeUndefined();
 	});
 
+	it("does not re-apply the native tool-set transform to an already transformed payload", () => {
+		const payload = {
+			tools: [{ type: "function", function: { name: "batch_computer_actions" } }],
+		};
+		const next = yutori.yutoriNativeToolSetOnPayload(payload, { id: "n1.5-latest" } as never);
+		const reapplied = yutori.yutoriNativeToolSetOnPayload(next, { id: "n1.5-latest" } as never);
+		expect(reapplied).toBeUndefined();
+	});
+
 	it("returns undefined for non-object payloads", () => {
 		expect(yutori.yutoriBuiltinToolsOnPayload(undefined)).toBeUndefined();
 		expect(yutori.yutoriBuiltinToolsOnPayload("x")).toBeUndefined();

You can send follow-ups to the cloud agent here.

Comment thread packages/ai/src/providers/yutori/provider.ts Outdated
Comment thread packages/ai/src/runtime-spec.ts
@rgarcia
rgarcia force-pushed the hypeship/yutori-native-tools branch 5 times, most recently from 4046268 to 64e9f90 Compare May 16, 2026 19:24
Comment thread packages/ai/src/runtime-spec.ts
@rgarcia
rgarcia force-pushed the hypeship/yutori-native-tools branch from 64e9f90 to 0c46fdb Compare May 16, 2026 19:31

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 4 total unresolved issues (including 3 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Multi-action expansion creates mismatched tool call IDs
    • Normalized expanded Yutori tool-call IDs back to their original IDs in outbound assistant/tool messages and collapsed duplicate expanded tool results to a single upstream response.

Create PR

Or push these changes by commenting:

@cursor push 4bcaf1dd5f
Preview (4bcaf1dd5f)
diff --git a/packages/ai/src/providers/yutori/provider.ts b/packages/ai/src/providers/yutori/provider.ts
--- a/packages/ai/src/providers/yutori/provider.ts
+++ b/packages/ai/src/providers/yutori/provider.ts
@@ -184,6 +184,7 @@
 
 function convertMessages(context: Context): ChatCompletionMessageParam[] {
 	const messages: ChatCompletionMessageParam[] = [];
+	const expandedToolCallIdMap = new Map<string, string>();
 	if (context.systemPrompt) messages.push({ role: "system", content: context.systemPrompt });
 	for (const message of context.messages) {
 		if (message.role === "user") {
@@ -196,29 +197,58 @@
 				.filter((part): part is TextContent => part.type === "text")
 				.map((part) => part.text)
 				.join("");
+			const seenExpandedToolCallIds = new Set<string>();
 			const toolCalls = message.content
 				.filter((part): part is ToolCall => part.type === "toolCall")
-				.map((part) => ({
-					id: part.id,
-					type: "function" as const,
-					function: { name: part.name, arguments: JSON.stringify(part.arguments ?? {}) },
-				}));
+				.flatMap((part) => {
+					const originalId = originalYutoriToolCallId(part.id, part.name);
+					if (originalId !== part.id) {
+						expandedToolCallIdMap.set(part.id, originalId);
+						if (seenExpandedToolCallIds.has(originalId)) return [];
+						seenExpandedToolCallIds.add(originalId);
+					}
+					return [
+						{
+							id: originalId,
+							type: "function" as const,
+							function: { name: part.name, arguments: JSON.stringify(part.arguments ?? {}) },
+						},
+					];
+				});
 			messages.push({
 				role: "assistant",
 				content: text || null,
 				...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
 			});
 		} else if (message.role === "toolResult") {
-			messages.push({
+			const toolCallId = expandedToolCallIdMap.get(message.toolCallId) ?? message.toolCallId;
+			const toolMessage = {
 				role: "tool",
-				tool_call_id: message.toolCallId,
+				tool_call_id: toolCallId,
 				content: message.content.map(toOpenAIContentPart) as unknown as string,
-			});
+			} as ChatCompletionMessageParam;
+			const previous = messages[messages.length - 1];
+			if (toolCallId !== message.toolCallId && isToolMessageParam(previous) && previous.tool_call_id === toolCallId) {
+				messages[messages.length - 1] = toolMessage;
+			} else {
+				messages.push(toolMessage);
+			}
 		}
 	}
 	return messages;
 }
 
+function originalYutoriToolCallId(id: string, name: string): string {
+	if (!isYutoriLocalActionToolName(name)) return id;
+	const match = id.match(/^(.*)_\d+$/);
+	if (!match?.[1]) return id;
+	return match[1];
+}
+
+function isToolMessageParam(message: ChatCompletionMessageParam | undefined): message is ChatCompletionMessageParam & { role: "tool" } {
+	return Boolean(message && message.role === "tool");
+}
+
 function convertTools(context: Context): Array<Record<string, unknown>> {
 	return (context.tools ?? []).map((tool) => ({
 		type: "function",

You can send follow-ups to the cloud agent here.

Comment thread packages/ai/src/providers/yutori/provider.ts
@rgarcia
rgarcia force-pushed the hypeship/yutori-native-tools branch 2 times, most recently from 51f3aea to 93fa003 Compare May 19, 2026 16:07

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 5 total unresolved issues (including 3 from previous reviews).

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Tzafon readNumber makes coordinate validation dead code
    • Changed Tzafon readNumber to return undefined for missing/non-finite values and added explicit usage defaults so coordinate checks and fallback fields now work as intended.
  • ✅ Fixed: Duplicate canonicalToolCallName/Arguments across Tzafon and Yutori providers
    • Removed the duplicated Tzafon helper implementations and imported the existing exported canonical helper functions from yutori/actions.ts.

Create PR

Or push these changes by commenting:

@cursor push 88b05b5349
Preview (88b05b5349)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -45,7 +45,7 @@
           GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
           TZAFON_API_KEY: ${{ secrets.TZAFON_API_KEY }}
           YUTORI_API_KEY: ${{ secrets.YUTORI_API_KEY }}
-        run: npm test --workspace @onkernel/cua-ai -- test/batch-tool.integration.test.ts
+        run: npm test --workspace @onkernel/cua-ai -- test/computer-tool.integration.test.ts
 
   agent-e2e:
     runs-on: ubuntu-latest

diff --git a/package-lock.json b/package-lock.json
--- a/package-lock.json
+++ b/package-lock.json
@@ -736,6 +736,16 @@
         "zod-to-json-schema": "^3.25.0"
       }
     },
+    "node_modules/@emnapi/runtime": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
+      "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
     "node_modules/@esbuild/aix-ppc64": {
       "version": "0.27.7",
       "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
@@ -1201,6 +1211,471 @@
         }
       }
     },
+    "node_modules/@img/colour": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
+      "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@img/sharp-darwin-arm64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
+      "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-darwin-arm64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-darwin-x64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
+      "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-darwin-x64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-libvips-darwin-arm64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
+      "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-darwin-x64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
+      "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-arm": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
+      "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-arm64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
+      "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-ppc64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
+      "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-riscv64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
+      "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-s390x": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
+      "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-x64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
+      "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
+      "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
+      "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-linux-arm": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
+      "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-arm": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-arm64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
+      "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-arm64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-ppc64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
+      "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-ppc64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-riscv64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
+      "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
+      "cpu": [
+        "riscv64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-riscv64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-s390x": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
+      "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
+      "cpu": [
+        "s390x"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-s390x": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-x64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
+      "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-x64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linuxmusl-arm64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
+      "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linuxmusl-x64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
+      "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-wasm32": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
+      "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
+      "cpu": [
+        "wasm32"
+      ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/runtime": "^1.7.0"
+      },
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-win32-arm64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
+      "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-win32-ia32": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
+      "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
+      "cpu": [
+        "ia32"
+      ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-win32-x64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
+      "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
     "node_modules/@jridgewell/sourcemap-codec": {
       "version": "1.5.5",
       "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
@@ -2848,6 +3323,15 @@
         "node": ">= 14"
       }
     },
+    "node_modules/detect-libc": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+      "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/diff": {
       "version": "8.0.4",
       "license": "BSD-3-Clause",
@@ -3957,6 +4441,62 @@
       ],
       "license": "MIT"
     },
+    "node_modules/semver": {
+      "version": "7.8.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
+      "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/sharp": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
+      "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
+      "hasInstallScript": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@img/colour": "^1.0.0",
+        "detect-libc": "^2.1.2",
+        "semver": "^7.7.3"
+      },
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-darwin-arm64": "0.34.5",
+        "@img/sharp-darwin-x64": "0.34.5",
+        "@img/sharp-libvips-darwin-arm64": "1.2.4",
+        "@img/sharp-libvips-darwin-x64": "1.2.4",
+        "@img/sharp-libvips-linux-arm": "1.2.4",
+        "@img/sharp-libvips-linux-arm64": "1.2.4",
+        "@img/sharp-libvips-linux-ppc64": "1.2.4",
+        "@img/sharp-libvips-linux-riscv64": "1.2.4",
+        "@img/sharp-libvips-linux-s390x": "1.2.4",
+        "@img/sharp-libvips-linux-x64": "1.2.4",
+        "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
+        "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
+        "@img/sharp-linux-arm": "0.34.5",
+        "@img/sharp-linux-arm64": "0.34.5",
+        "@img/sharp-linux-ppc64": "0.34.5",
+        "@img/sharp-linux-riscv64": "0.34.5",
+        "@img/sharp-linux-s390x": "0.34.5",
+        "@img/sharp-linux-x64": "0.34.5",
+        "@img/sharp-linuxmusl-arm64": "0.34.5",
+        "@img/sharp-linuxmusl-x64": "0.34.5",
+        "@img/sharp-wasm32": "0.34.5",
+        "@img/sharp-win32-arm64": "0.34.5",
+        "@img/sharp-win32-ia32": "0.34.5",
+        "@img/sharp-win32-x64": "0.34.5"
+      }
+    },
     "node_modules/siginfo": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
@@ -4623,6 +5163,7 @@
         "@earendil-works/pi-ai": "^0.74.0",
         "@onkernel/cua-ai": "0.1.0",
         "@onkernel/sdk": "0.49.0",
+        "sharp": "^0.34.5",
         "typebox": "^1.1.38"
       },
       "devDependencies": {

diff --git a/packages/agent/README.md b/packages/agent/README.md
--- a/packages/agent/README.md
+++ b/packages/agent/README.md
@@ -84,6 +84,9 @@
 - `browser` (Kernel browser response)
 - `client` (Kernel SDK client)
 - CUA model refs (`"provider:model"`) accepted where pi expects a concrete model
+- `extraTools` for caller-owned tools appended after built-in CUA tools
+- `synthesizeBatchTool: true` to add a local `batch_computer_actions` helper
+- `computerUseExtraTool: true` to add the local `computer_use_extra` helper
 
 If auth callbacks are omitted, both classes default to CUA env var conventions:
 - OpenAI: `OPENAI_API_KEY`
@@ -94,10 +97,15 @@
 
 ### Tool Defaults
 
-If tools are omitted, the classes install canonical CUA computer tool executors
-using runtime specs from `@onkernel/cua-ai`. If tools are provided, they are
-used exactly.
+By default, the classes install provider-selected canonical CUA computer tool
+executors using runtime specs from `@onkernel/cua-ai`. Use `extraTools` to add
+caller-owned tools alongside the provider's computer-use tools.
 
+`batch_computer_actions` and `computer_use_extra` are opt-in CuaAgent and
+CuaAgentHarness sugar. They are synthesized from the provider's canonical
+action definitions and can be enabled with `synthesizeBatchTool` and
+`computerUseExtraTool`.
+
 ### Model Switching
 
 `CuaAgent` follows pi `Agent` semantics: assign `agent.state.model` to a
@@ -124,6 +132,7 @@
     browser,
     client,
     toolDefinitions: runtime.toolDefinitions,
+    synthesizeBatchTool: true,
   }),
   myCustomTool,
 ];

diff --git a/packages/agent/package.json b/packages/agent/package.json
--- a/packages/agent/package.json
+++ b/packages/agent/package.json
@@ -44,6 +44,7 @@
     "@earendil-works/pi-ai": "^0.74.0",
     "@onkernel/cua-ai": "0.1.0",
     "@onkernel/sdk": "0.49.0",
+    "sharp": "^0.34.5",
     "typebox": "^1.1.38"
   },
   "devDependencies": {

diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts
--- a/packages/agent/src/agent.ts
+++ b/packages/agent/src/agent.ts
@@ -11,6 +11,8 @@
 } from "./vendor/pi-agent-core/index";
 import {
 	type Api,
+	CUA_BATCH_TOOL_NAME,
+	CUA_NAVIGATION_TOOL_NAME,
 	type CuaModelRef,
 	getCuaEnvApiKey,
 	type Model,
@@ -20,7 +22,7 @@
 } from "@onkernel/cua-ai";
 import type Kernel from "@onkernel/sdk";
 import { createCuaComputerTools } from "./tools";
-import type { KernelBrowser } from "./translator/translator";
+import { InternalComputerTranslator, type KernelBrowser } from "./translator/translator";
 
 /** A CUA model reference string or a concrete pi model object. */
 type CuaRuntimeInput = CuaModelRef | Model<Api>;
@@ -45,8 +47,6 @@
 type CuaAgentInitialState = Omit<NonNullable<AgentOptions["initialState"]>, "model" | "tools"> & {
 	/** Model to use for the first turn. CUA refs are resolved before pi sees the state. */
 	model: CuaRuntimeInput;
-	/** Optional caller-owned tools. Omit this to install the provider's default CUA tools. */
-	tools?: AgentTool[];
 };
 
 /**
@@ -63,6 +63,12 @@
 	client: Kernel;
 	/** Initial pi state plus a CUA-aware model value. */
 	initialState: CuaAgentInitialState;
+	/** Additional caller-owned tools appended after built-in CUA tools. */
+	extraTools?: AgentTool[];
+	/** Add a local batch_computer_actions helper synthesized from provider action definitions. */
+	synthesizeBatchTool?: boolean;
+	/** Add the local computer_use_extra navigation helper. */
+	computerUseExtraTool?: boolean;
 };
 
 /**
@@ -75,13 +81,19 @@
 export type CuaAgentHarnessOptions<
 	TSkill extends Skill = Skill,
 	TPromptTemplate extends PromptTemplate = PromptTemplate,
-> = Omit<AgentHarnessOptions<TSkill, TPromptTemplate, AgentTool>, "model"> & {
+> = Omit<AgentHarnessOptions<TSkill, TPromptTemplate, AgentTool>, "model" | "tools"> & {
 	/** Kernel browser session used by default CUA tools. */
 	browser: KernelBrowser;
 	/** Kernel SDK client used by default CUA tools. */
 	client: Kernel;
 	/** Model used by the harness. CUA refs are resolved before pi sees the model. */
 	model: CuaRuntimeInput;
+	/** Additional caller-owned tools appended after built-in CUA tools. */
+	extraTools?: AgentTool[];
+	/** Add a local batch_computer_actions helper synthesized from provider action definitions. */
+	synthesizeBatchTool?: boolean;
+	/** Add the local computer_use_extra navigation helper. */
+	computerUseExtraTool?: boolean;
 	/** Optional payload hook composed after the provider-specific CUA payload hook. */
 	onPayload?: SimpleStreamOptions["onPayload"];
 };
@@ -89,9 +101,10 @@
 /**
  * Holds the CUA-specific pieces that have to change when a model changes.
  *
- * If callers omit `tools` or `systemPrompt`, CUA owns those values and refreshes
- * them from `@onkernel/cua-ai` whenever the model changes. If callers pass
- * their own tools or prompt, the controller preserves those caller-owned values.
+ * CUA owns the computer-use tools and refreshes them from `@onkernel/cua-ai`
+ * whenever the model changes. Caller-owned `extraTools` are appended after
+ * those defaults. If callers pass their own prompt, the controller preserves
+ * that caller-owned prompt.
  */
 class CuaRuntimeController {
 	private runtimeSpec: CuaRuntimeSpec;
@@ -101,7 +114,9 @@
 			browser: KernelBrowser;
 			client: Kernel;
 			model: CuaRuntimeInput;
-			tools?: AgentTool[];
+			extraTools?: AgentTool[];
+			synthesizeBatchTool?: boolean;
+			computerUseExtraTool?: boolean;
 			systemPrompt?: unknown;
 			onPayload?: SimpleStreamOptions["onPayload"];
 		},
@@ -114,7 +129,7 @@
 	}
 
 	get ownsTools(): boolean {
-		return this.options.tools === undefined;
+		return true;
 	}
 
 	get ownsSystemPrompt(): boolean {
@@ -130,20 +145,80 @@
 	}
 
 	tools(): AgentTool[] {
-		return (
-			this.options.tools ??
-			createCuaComputerTools({
+		return [
+			...createCuaComputerTools({
 				browser: this.options.browser,
 				client: this.options.client,
 				toolDefinitions: this.runtimeSpec.toolDefinitions,
-			})
-		);
+				coordinateSystem: this.runtimeSpec.coordinateSystem,
+				screenshot: this.runtimeSpec.screenshot,
+				synthesizeBatchTool: this.options.synthesizeBatchTool,
+				computerUseExtraTool: this.options.computerUseExtraTool,
+			}),
+			...(this.options.extraTools ?? []),
+		];
 	}
 
 	onPayloadFor(model: CuaRuntimeInput): SimpleStreamOptions["onPayload"] {
 		const runtimeSpec = resolveCuaRuntimeSpec(model);
-		return composeOnPayload(runtimeSpec.onPayload, this.options.onPayload);
+		return composeOnPayload(
+			composeOnPayload(this.screenshotOnPayload(runtimeSpec), this.providerOnPayload(runtimeSpec)),
+			this.options.onPayload,
+		);
 	}
+
+	keepToolNames(): string[] {
+		return [
+			...(this.options.extraTools ?? []).map((tool) => tool.name),
+			...(this.options.synthesizeBatchTool ? [CUA_BATCH_TOOL_NAME] : []),
+			...(this.options.computerUseExtraTool ? [CUA_NAVIGATION_TOOL_NAME] : []),
+		];
+	}
+
+	private providerOnPayload(runtimeSpec: CuaRuntimeSpec): SimpleStreamOptions["onPayload"] | undefined {
+		if (!runtimeSpec.onPayload) return undefined;
+		return async (payload, model) =>
+			runtimeSpec.onPayload?.(payload, model as Model<Api>, { keepToolNames: this.keepToolNames() });
+	}
+
+	private screenshotOnPayload(runtimeSpec: CuaRuntimeSpec): SimpleStreamOptions["onPayload"] | undefined {
+		if (!runtimeSpec.screenshot?.appendToLatestMessage) return undefined;
+		return async (payload) => {
+			if (!payload || typeof payload !== "object") return undefined;
+			const current = payload as { messages?: unknown };
+			if (!Array.isArray(current.messages) || current.messages.length === 0) return undefined;
+			const last = current.messages[current.messages.length - 1];
+			if (!last || typeof last !== "object") return undefined;
+			const lastMessage = last as { content?: unknown; role?: unknown };
+			if (lastMessage.role !== "user" && lastMessage.role !== "tool") return undefined;
... diff truncated: showing 800 of 3812 lines

You can send follow-ups to the cloud agent here.

Comment thread packages/ai/src/providers/tzafon/provider.ts
Comment thread packages/ai/src/providers/tzafon/provider.ts Outdated
@rgarcia
rgarcia force-pushed the hypeship/yutori-native-tools branch 5 times, most recently from ab0c7c1 to 360dba8 Compare May 20, 2026 15:19

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Tzafon reuses tool call IDs
    • Tzafon now suffixes expanded computer-call IDs when one native action maps to multiple canonical tool calls, preserving unique toolCallId correlation.

Create PR

Or push these changes by commenting:

@cursor push 0520d9fe7d
Preview (0520d9fe7d)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -45,7 +45,7 @@
           GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
           TZAFON_API_KEY: ${{ secrets.TZAFON_API_KEY }}
           YUTORI_API_KEY: ${{ secrets.YUTORI_API_KEY }}
-        run: npm test --workspace @onkernel/cua-ai -- test/batch-tool.integration.test.ts
+        run: npm test --workspace @onkernel/cua-ai -- test/computer-tool.integration.test.ts
 
   agent-e2e:
     runs-on: ubuntu-latest

diff --git a/package-lock.json b/package-lock.json
--- a/package-lock.json
+++ b/package-lock.json
@@ -736,6 +736,16 @@
         "zod-to-json-schema": "^3.25.0"
       }
     },
+    "node_modules/@emnapi/runtime": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
+      "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
     "node_modules/@esbuild/aix-ppc64": {
       "version": "0.27.7",
       "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
@@ -1201,6 +1211,471 @@
         }
       }
     },
+    "node_modules/@img/colour": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
+      "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@img/sharp-darwin-arm64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
+      "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-darwin-arm64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-darwin-x64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
+      "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-darwin-x64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-libvips-darwin-arm64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
+      "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-darwin-x64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
+      "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-arm": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
+      "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-arm64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
+      "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-ppc64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
+      "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-riscv64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
+      "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-s390x": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
+      "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-x64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
+      "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
+      "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
+      "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-linux-arm": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
+      "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-arm": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-arm64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
+      "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-arm64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-ppc64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
+      "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-ppc64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-riscv64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
+      "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
+      "cpu": [
+        "riscv64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-riscv64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-s390x": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
+      "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
+      "cpu": [
+        "s390x"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-s390x": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-x64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
+      "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-x64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linuxmusl-arm64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
+      "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linuxmusl-x64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
+      "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-wasm32": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
+      "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
+      "cpu": [
+        "wasm32"
+      ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/runtime": "^1.7.0"
+      },
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-win32-arm64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
+      "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-win32-ia32": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
+      "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
+      "cpu": [
+        "ia32"
+      ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-win32-x64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
+      "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
     "node_modules/@jridgewell/sourcemap-codec": {
       "version": "1.5.5",
       "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
@@ -2848,6 +3323,15 @@
         "node": ">= 14"
       }
     },
+    "node_modules/detect-libc": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+      "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/diff": {
       "version": "8.0.4",
       "license": "BSD-3-Clause",
@@ -3957,6 +4441,62 @@
       ],
       "license": "MIT"
     },
+    "node_modules/semver": {
+      "version": "7.8.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
+      "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/sharp": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
+      "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
+      "hasInstallScript": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@img/colour": "^1.0.0",
+        "detect-libc": "^2.1.2",
+        "semver": "^7.7.3"
+      },
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-darwin-arm64": "0.34.5",
+        "@img/sharp-darwin-x64": "0.34.5",
+        "@img/sharp-libvips-darwin-arm64": "1.2.4",
+        "@img/sharp-libvips-darwin-x64": "1.2.4",
+        "@img/sharp-libvips-linux-arm": "1.2.4",
+        "@img/sharp-libvips-linux-arm64": "1.2.4",
+        "@img/sharp-libvips-linux-ppc64": "1.2.4",
+        "@img/sharp-libvips-linux-riscv64": "1.2.4",
+        "@img/sharp-libvips-linux-s390x": "1.2.4",
+        "@img/sharp-libvips-linux-x64": "1.2.4",
+        "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
+        "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
+        "@img/sharp-linux-arm": "0.34.5",
+        "@img/sharp-linux-arm64": "0.34.5",
+        "@img/sharp-linux-ppc64": "0.34.5",
+        "@img/sharp-linux-riscv64": "0.34.5",
+        "@img/sharp-linux-s390x": "0.34.5",
+        "@img/sharp-linux-x64": "0.34.5",
+        "@img/sharp-linuxmusl-arm64": "0.34.5",
+        "@img/sharp-linuxmusl-x64": "0.34.5",
+        "@img/sharp-wasm32": "0.34.5",
+        "@img/sharp-win32-arm64": "0.34.5",
+        "@img/sharp-win32-ia32": "0.34.5",
+        "@img/sharp-win32-x64": "0.34.5"
+      }
+    },
     "node_modules/siginfo": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
@@ -4623,6 +5163,7 @@
         "@earendil-works/pi-ai": "^0.74.0",
         "@onkernel/cua-ai": "0.1.0",
         "@onkernel/sdk": "0.49.0",
+        "sharp": "^0.34.5",
         "typebox": "^1.1.38"
       },
       "devDependencies": {

diff --git a/packages/agent/README.md b/packages/agent/README.md
--- a/packages/agent/README.md
+++ b/packages/agent/README.md
@@ -84,6 +84,9 @@
 - `browser` (Kernel browser response)
 - `client` (Kernel SDK client)
 - CUA model refs (`"provider:model"`) accepted where pi expects a concrete model
+- `extraTools` to add your own pi tools alongside the built-in browser tools
+- `batchTool: true` to let the model run multiple browser actions in one tool call
+- `computerUseExtra: true` to let the model use a small navigation helper
 
 If auth callbacks are omitted, both classes default to CUA env var conventions:
 - OpenAI: `OPENAI_API_KEY`
@@ -94,10 +97,27 @@
 
 ### Tool Defaults
 
-If tools are omitted, the classes install canonical CUA computer tool executors
-using runtime specs from `@onkernel/cua-ai`. If tools are provided, they are
-used exactly.
+By default, the classes install provider-selected canonical CUA computer tool
+executors using runtime specs from `@onkernel/cua-ai`. Use `extraTools` to add
+your own pi tools alongside the provider's computer-use tools. This is useful
+when the model needs to call application-specific code, such as looking up a
+record, writing a database row, or handing off to another service while it also
+controls the browser.
 
+`batchTool: true` adds the `batch_computer_actions` tool. Use it when you want
+the model to group several browser actions into one call, for example moving,
+clicking, typing, waiting, and then reading a screenshot. The batch tool is
+synthesized from the selected provider's normal browser action definitions, so
+it only batches actions that provider runtime already supports.
+
+`computerUseExtra: true` adds the `computer_use_extra` tool. Use it when you
+want one compact helper for common browser navigation/read operations:
+`goto`, `back`, `forward`, and `url`.
+
+The TypeScript API follows pi's camelCase option style (`extraTools`,
+`batchTool`, `computerUseExtra`). Names like `batch_computer_actions` and
+`computer_use_extra` are the literal tool names the model may see in traces.
+
 ### Model Switching
 
 `CuaAgent` follows pi `Agent` semantics: assign `agent.state.model` to a
@@ -124,6 +144,7 @@
     browser,
     client,
     toolDefinitions: runtime.toolDefinitions,
+    batchTool: true,
   }),
   myCustomTool,
 ];

diff --git a/packages/agent/package.json b/packages/agent/package.json
--- a/packages/agent/package.json
+++ b/packages/agent/package.json
@@ -44,6 +44,7 @@
     "@earendil-works/pi-ai": "^0.74.0",
     "@onkernel/cua-ai": "0.1.0",
     "@onkernel/sdk": "0.49.0",
+    "sharp": "^0.34.5",
     "typebox": "^1.1.38"
   },
   "devDependencies": {

diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts
--- a/packages/agent/src/agent.ts
+++ b/packages/agent/src/agent.ts
@@ -11,6 +11,8 @@
 } from "./vendor/pi-agent-core/index";
 import {
 	type Api,
+	CUA_BATCH_TOOL_NAME,
+	CUA_NAVIGATION_TOOL_NAME,
 	type CuaModelRef,
 	getCuaEnvApiKey,
 	type Model,
@@ -20,7 +22,7 @@
 } from "@onkernel/cua-ai";
 import type Kernel from "@onkernel/sdk";
 import { createCuaComputerTools } from "./tools";
-import type { KernelBrowser } from "./translator/translator";
+import { InternalComputerTranslator, type KernelBrowser } from "./translator/translator";
 
 /** A CUA model reference string or a concrete pi model object. */
 type CuaRuntimeInput = CuaModelRef | Model<Api>;
@@ -45,8 +47,6 @@
 type CuaAgentInitialState = Omit<NonNullable<AgentOptions["initialState"]>, "model" | "tools"> & {
 	/** Model to use for the first turn. CUA refs are resolved before pi sees the state. */
 	model: CuaRuntimeInput;
-	/** Optional caller-owned tools. Omit this to install the provider's default CUA tools. */
-	tools?: AgentTool[];
 };
 
 /**
@@ -63,6 +63,12 @@
 	client: Kernel;
 	/** Initial pi state plus a CUA-aware model value. */
 	initialState: CuaAgentInitialState;
+	/** Add your own pi tools alongside the built-in browser tools. */
+	extraTools?: AgentTool[];
+	/** Expose a batch tool so the model can run multiple browser actions in one call. */
+	batchTool?: boolean;
+	/** Expose a helper for browser navigation and URL reads. */
+	computerUseExtra?: boolean;
 };
 
 /**
@@ -75,13 +81,19 @@
 export type CuaAgentHarnessOptions<
 	TSkill extends Skill = Skill,
 	TPromptTemplate extends PromptTemplate = PromptTemplate,
-> = Omit<AgentHarnessOptions<TSkill, TPromptTemplate, AgentTool>, "model"> & {
+> = Omit<AgentHarnessOptions<TSkill, TPromptTemplate, AgentTool>, "model" | "tools"> & {
 	/** Kernel browser session used by default CUA tools. */
 	browser: KernelBrowser;
 	/** Kernel SDK client used by default CUA tools. */
 	client: Kernel;
 	/** Model used by the harness. CUA refs are resolved before pi sees the model. */
 	model: CuaRuntimeInput;
+	/** Add your own pi tools alongside the built-in browser tools. */
+	extraTools?: AgentTool[];
+	/** Expose a batch tool so the model can run multiple browser actions in one call. */
+	batchTool?: boolean;
+	/** Expose a helper for browser navigation and URL reads. */
+	computerUseExtra?: boolean;
 	/** Optional payload hook composed after the provider-specific CUA payload hook. */
 	onPayload?: SimpleStreamOptions["onPayload"];
 };
@@ -89,9 +101,10 @@
 /**
  * Holds the CUA-specific pieces that have to change when a model changes.
  *
- * If callers omit `tools` or `systemPrompt`, CUA owns those values and refreshes
- * them from `@onkernel/cua-ai` whenever the model changes. If callers pass
- * their own tools or prompt, the controller preserves those caller-owned values.
+ * CUA owns the computer-use tools and refreshes them from `@onkernel/cua-ai`
+ * whenever the model changes. Caller-owned `extraTools` are appended after
+ * those defaults. If callers pass their own prompt, the controller preserves
+ * that caller-owned prompt.
  */
 class CuaRuntimeController {
 	private runtimeSpec: CuaRuntimeSpec;
@@ -101,7 +114,9 @@
 			browser: KernelBrowser;
 			client: Kernel;
 			model: CuaRuntimeInput;
-			tools?: AgentTool[];
+			extraTools?: AgentTool[];
+			batchTool?: boolean;
+			computerUseExtra?: boolean;
 			systemPrompt?: unknown;
 			onPayload?: SimpleStreamOptions["onPayload"];
 		},
@@ -114,7 +129,7 @@
 	}
 
 	get ownsTools(): boolean {
-		return this.options.tools === undefined;
+		return true;
 	}
 
 	get ownsSystemPrompt(): boolean {
@@ -130,20 +145,80 @@
 	}
 
 	tools(): AgentTool[] {
-		return (
-			this.options.tools ??
-			createCuaComputerTools({
+		return [
+			...createCuaComputerTools({
 				browser: this.options.browser,
 				client: this.options.client,
 				toolDefinitions: this.runtimeSpec.toolDefinitions,
-			})
-		);
+				coordinateSystem: this.runtimeSpec.coordinateSystem,
+				screenshot: this.runtimeSpec.screenshot,
+				batchTool: this.options.batchTool,
+				computerUseExtra: this.options.computerUseExtra,
+			}),
+			...(this.options.extraTools ?? []),
+		];
 	}
 
 	onPayloadFor(model: CuaRuntimeInput): SimpleStreamOptions["onPayload"] {
 		const runtimeSpec = resolveCuaRuntimeSpec(model);
-		return composeOnPayload(runtimeSpec.onPayload, this.options.onPayload);
+		return composeOnPayload(
+			composeOnPayload(this.screenshotOnPayload(runtimeSpec), this.providerOnPayload(runtimeSpec)),
+			this.options.onPayload,
+		);
 	}
+
+	keepToolNames(): string[] {
+		return [
+			...(this.options.extraTools ?? []).map((tool) => tool.name),
+			...(this.options.batchTool ? [CUA_BATCH_TOOL_NAME] : []),
+			...(this.options.computerUseExtra ? [CUA_NAVIGATION_TOOL_NAME] : []),
+		];
+	}
+
+	private providerOnPayload(runtimeSpec: CuaRuntimeSpec): SimpleStreamOptions["onPayload"] | undefined {
+		if (!runtimeSpec.onPayload) return undefined;
+		return async (payload, model) =>
+			runtimeSpec.onPayload?.(payload, model as Model<Api>, { keepToolNames: this.keepToolNames() });
... diff truncated: showing 800 of 3979 lines

You can send follow-ups to the cloud agent here.

Comment thread packages/ai/src/providers/tzafon/provider.ts
@rgarcia
rgarcia force-pushed the hypeship/yutori-native-tools branch from 360dba8 to 810ad63 Compare May 20, 2026 15:23
@rgarcia
rgarcia force-pushed the hypeship/yutori-native-tools branch 4 times, most recently from 9e69706 to 350ca08 Compare May 20, 2026 16:11
@rgarcia
rgarcia force-pushed the hypeship/yutori-native-tools branch from 350ca08 to 53f7e28 Compare May 20, 2026 16:38

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Tzafon hscroll scrolls at origin
    • The hscroll normalization now preserves optional x/y coordinates when building canonical scroll actions, preventing default translation to (0,0).

Create PR

Or push these changes by commenting:

@cursor push 75aebfccd6
Preview (75aebfccd6)
diff --git a/packages/ai/src/providers/tzafon/provider.ts b/packages/ai/src/providers/tzafon/provider.ts
--- a/packages/ai/src/providers/tzafon/provider.ts
+++ b/packages/ai/src/providers/tzafon/provider.ts
@@ -207,7 +207,14 @@
 		case "scroll":
 			return [toScrollAction(current)];
 		case "hscroll":
-			return [{ type: "scroll", scroll_x: readOptionalNumber(current, "scroll_x") ?? readOptionalNumber(current, "amount") ?? 0 }];
+			return [
+				{
+					type: "scroll",
+					x,
+					y,
+					scroll_x: readOptionalNumber(current, "scroll_x") ?? readOptionalNumber(current, "amount") ?? 0,
+				},
+			];
 		case "navigate":
 			return [{ type: "goto", url: getString(current, "url") }];
 		case "wait":

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 53f7e28. Configure here.

case "scroll":
return [toScrollAction(current)];
case "hscroll":
return [{ type: "scroll", scroll_x: readOptionalNumber(current, "scroll_x") ?? readOptionalNumber(current, "amount") ?? 0 }];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tzafon hscroll scrolls at origin

Medium Severity

When Tzafon returns a native hscroll computer action, normalization builds a canonical scroll with only scroll_x and no pointer position. The agent translator then sends the scroll to Kernel at viewport coordinates (0, 0) instead of the intended location, so horizontal scrolling can hit the wrong UI region.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 53f7e28. Configure here.

@rgarcia
rgarcia merged commit 602e2a9 into main May 20, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant